<!DOCTYPE html>
<title>Variables</title>

<style type="text/css">
    body { background-color: #fec; font-family: sans-serif }
    .inlineButton { margin-left: 5px }
    .inlineAnswer { margin-left: 5px; color: gray }
    .advice { background-color: lightgreen; color: darkgreen; border-style: outset; border-width: thin; border-color: lightgreen; padding: 3px; font-size: x-small }
    .sectionHeader { font-size: large; text-align: left }
    .header-left { text-align: left }
    .padleft { padding-left: 10px }
    #variablesDiv { font-family: sans-serif; margin-top: 0.25in }
    #variablesTable { font-size: small }
    .variableControl { font-size: small }
    .checkboxColumn { text-align: center }
    .clickableValue { color: blue; cursor: pointer }
    .clickableVariable { color: blue; cursor: pointer }
    .modifyValue { border-width: thin; border-color: black; border-style: outset; padding: 6px; background-color: lightgray }
</style>

<script type="text/javascript" src="protovis-d3.2.js"></script>
<script type="text/javascript" src="../visualization/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript" src="eddata.js"></script>
<!-- 
   data.js defines a variable 'data', a rectangular array of text strings (some of which may represent numbers). 
     We expect the first row to contain column labels. 
   studentMCAS.js defines studentMCAS, a similar array containing student MCAS histories per subject.
     In this array numbers are already numbers and missing values are already nulls.
   For X61 notebook:
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/data.js"></script>
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/student MCAS.js"></script>
   For Dell desktop at work:
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/Desktop/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/data.js"></script>
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/Desktop/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/student MCAS.js"></script>
-->
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/data.js"></script>
<script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/student MCAS.js"></script>


<script type="text/javascript">

// When the DOM is ready, begin.
$(begin);

var vars = [];  // our variables, to be constructed

function scrollConversation() {
    $('body > *:last')[0].scrollIntoView(false);
}

function begin() {
    $('body').html('<p>Hello.');
    if (!data) {
        $('body').append('<p>You don\'t seem to have any data. Perhaps the path to your data file is incorrect?');
    } else {
        $('body').append('<p>There is data here. I see these labels in the first row of your data array:');
        $('body').append('<ul>' + data[0].map(function(label) { return '<li>' + safe(label) } ).join('\n') + '</ul>');
        $('body').append('<p>I count ' + (data.length - 1) + ' rows of data following the labels.');
        $('body').append('<p>Does this seem right?' +
                '<span>' +
                '<button type="button" class="inlineButton" onclick="labelsSeemOk(this)">yes</button>' +
                '<button type="button" class="inlineButton" onclick="labelsSeemWrong(this)">no</button>' +
                '</span>');
    }
}

function labelsSeemWrong(button) {
    $(button).closest('span').replaceWith('<span class="inlineAnswer">no</span>');
    $('body').append('<p>I\'m sorry! Prepare a data file as follows.');
    scrollConversation();
    $('body').append('<div id="dataFileInstructions"></div>');
    var steps = [
        'You have a set of variable values for a set of occurrences. You might have names and test scores for a set of students, or you might have item numbers, topics, and average scores for a set of test items.',
        'Place a label for each variable in the first line of the file, with a tab character between each label: <code>label 1&rarr;label 2&rarr;label 3</code>.',
        'Each subsequent line represents one occurrence. Include the value for each variable, in the same order as the labels on the first line. Separate values with tab characters: <code>value1&rarr;value2&rarr;value3</code>.',
        'If a variable has no value for a particular line, leave it out but keep the tab characters that would separate it from its neighbors.' +
            'For example, if the second value on a line is missing the line should start like this: <code>val1&rarr;&rarr;val3</code>.'
        ];
    $('#dataFileInstructions')
        .append('<ol>' + steps.map(function(s) { return '<li>' + s } ).join('\n') + '</ol>')
        ;
    scrollConversation();
}

function labelsSeemOk(button) {
    $(button).closest('span').replaceWith('<span class="inlineAnswer">yes</span>');
    $('body').append('<p>Okay. Now we\'ll examine the variables and classify them.');
    $('body').append('<button type="button" onclick="nowLookAtVariables()">OK</button>');
    scrollConversation();
}

function nowLookAtVariables() {
    $('body').html('<p>working...');
    var nRows = data.length - 1;
    vars = data.shift().map( 
        function(label, col) { 
            var t = new (classifyDataName(label))(nRows);  // create best-guess eddata variable preallocated length
            t.label = label;  // use supplied label
            t.position = col;  // remember column position
            return t;
        } 
    );
    // Populate and fix the variable arrays.
    for (var i = 0; i < vars.length; i++) {
        for (var row = 0; row < nRows; row++) {
            vars[i].v[row] = data[row][i];
            vars[i].fix(row);
        }
    }
    // Scan variable values to help guess types.
    EdDatum.prototype.maxUniqValuesToKeep = 10; // number of text values to store as sample values
    for (var i = 0; i < vars.length; i++) {
        vars[i].scanValues();
    }
    // Display and adjust variables.
    displayVariablesPage();
}

function displayVariablesPage() {
    $('body').html('<div id="variablesDiv"></div>');
    displayVariables();
    $('#variablesDiv')[0].scrollIntoView(true);
}

function exploreVariable(pos) {
    $('body').html('<p>' + safe(vars[pos].label).italics() +
            ' is currently classified as a ' + vars[pos].type.name + '.\n');
    if (vars[pos].uniqCount == 1 && vars[pos].hasMissing) {
        $('body').append('<p>It has no assigned values for any of the ' + data.length + ' rows.');
    } else if (vars[pos].uniqNumCount == 0) {
        $('body').append('<p>It has ' + vars[pos].uniqCount + ' unique values' + 
                (vars[pos].hasMissing ? ' including the \'missing\' value.' : ' and no missing values.'));
    }
    $('body').append('<button type="button" onclick="displayVariablesPage()">return</button>');
}

var wOverall = screen.availWidth * 0.9,  // be greedy with available screen space
    hOverall = screen.availHeight * 0.8;
//$('#visDiv').hide().width(wOverall).height(hOverall);  // size our host div, but hide it for now
var padding = { left: 30, right: 30, top: 30, bottom: 20 };
var w = wOverall - padding.left - padding.right,  // main visualization will be w x h pixels
    h = hOverall - padding.top - padding.bottom;

// The file data.js gave us data, an array of variable values in columns, labelled in the first row.
// Create eddata variables, one per column.
// Keep the list in the same order as the data columns.
// Keep the original data in the array.


function getSelection(menu) {
    // Get the selected option in a SELECT menu.
    return $('option:selected', menu).val();
}

function selectType(menu) {
    // Respond to variable type selection in a SELECT menu.
    menuNameExp = /column(\d+)TypeSelect/;
    var c;  // data column index
    var label;  // existing variable label
    var nValues = data.length;
    var r;  // data row index
    var fixed = [0, 0, 0];  // counts: unrepairable, fine as is, repaired
    var newType = eval($('option:selected', menu).val());
    var nameMatch = menuNameExp.exec(menu.name);
    if (nameMatch) {
        c = Number(nameMatch[1]);
        type = getSelection(menu);
        label = vars[c].label;
        vars[c] = new newType(nValues);
        vars[c].label = label;
        vars[c].position = c;
        for (r = 0; r < nValues; r++) {
            vars[c].v[r] = data[r][c];
            fixed[vars[c].fix(r) + 1]++;
        }
        vars[c].scanValues();  // rescan
        displayVariables();  // update display
        alert(fixed[1] + ' good values; ' + fixed[2] + ' values fixed; ' + fixed[0] + ' bad values');
    }
}

var displayCols = [];   // column indices of variables we're displaying at any given time
var filter = [];        // filters [not used yet]

function displayVariables() {
    // Create a variable display with controls to
    //    select variables for display ('show')
    //    set the variable type
    //    filter data set by variable values
    // Show value ranges.
    $('#variablesDiv').html('<form><table id="variablesTable">' +
            '<caption class="sectionHeader">Variables <span class="advice">Click on a variable name to explore it.</span></caption>' +
            '<tr>' +
            '<th class="header-left">name</th>' +
            //'<th class="checkboxColumn">show</th>' +
            '<th>type</th>' +
            //'<th>filter</th>' +
            '<th class="padleft header-left">values</th>' +
            '</tr></table></form>');
    vars.forEach(displayVar);
}

function displayVar(v) {
    // Build a display row for this variable.
    var tr = $('<tr><td class="varName"><span class="clickableVariable"' +
            'onclick="exploreVariable(' + v.position + ')">' + 
            safe(v.label) + '</span></td></tr>');
    //tr.append('<td class="checkboxColumn">' + '<input type="checkbox" name="column' + v.position + 'Show" onchange="showVar(this)" value="off">' + '</td>');
    tr.append('<td>' + varTypeSelectControl(v) + '</td>');
    //tr.append('<td>' + varValueSelectControl(v) + '</td>');
    tr.append('<td class="padleft">' + varValuesDisplay(v) + '</td>');
    $('#variablesTable').append(tr);
}

function varTypeSelectControl(v) {
    // Build a variable type selection control for this variable.
    // Names are uniform: column<n>TypeSelect, where <n> is the column position.
    // Preselect whatever type we've determined so far.
    var html = '<select name="column' + v.position + 'TypeSelect" class="variableControl" onchange="selectType(this)">';
    html += edDataTypes.map(
            function(t) { return '<option' + (t == v.type ? ' selected' : '') + '>' + t.name + '</option>' } );
    html += '</select>';
    return html;
}

function varValueSelectControl(v) {
    // Stub.
    var html = '<select multiple name="column' + v.position + 'ValueSelect" class="variableControl" onchange="includeValues(this)">';
    html += '</select>';
    return '';
}

function varValuesDisplay(v) {
    // Text to display variable values, and provide links to manipulate them.
    var text = '';
    if (v.mostlyNum) {
        text += v.min + ' - ' + v.max;
        if (v.uniqTextCount) {
            text += ', ';
            text += v.uniqValues.map(varValueDisplay, v).join(', ');
            if (v.uniqTextCount > v.uniqValues.length) text += ',...';
            text += ' <span class="advice">Click on a value to remove it or substitute a numeric value.</span>'
        }
    }
    else if (v.uniqTextCount) {
        text += safe(v.uniqValues.join(', '));
        if (v.uniqTextCount > v.uniqValues.length) text += ',...';
    }
    return text;
}

function varValueDisplay(val) {
    // Create a clickable value display span for this value and the variable in context ('this').
    return '<span class="clickableValue" onclick="modifyValue(' + this.position + ",'" + safe(val) + "'" + ', this)">' +
        safe(val) + '</span>';
}

function modifyValue(position, safeValue, span) {
    // Respond to a click on the value.
    var id='modify' + position + '-' + safeValue;
    var interaction = '<span class="modifyValue" id="' + id + '">' + safeValue +
        '<button type="button" onclick="modifyValClick(this)">remove</button>' +
        '<button type="button" onclick="modifyValClick(this)">replace</button>' +
        '<button type="button" onclick="modifyValClick(this)">cancel</button></span>';
    $(span).replaceWith(interaction);
}

function modifyValClick(button) {
    // Respond to a button click on a modifyValue interaction panel.
    var idExp = /modify(\d+)-(.*)/;
    var request = $(button).text();  // 'remove', 'replace', 'cancel'
    var panel = $(button).closest('span.modifyValue');
    var panelId = panel.attr('id');
    var matchId = idExp.exec(panelId);
    if (matchId) {
        var position = matchId[1];
        var value = unSafe(matchId[2]);
    }
    else 
        throw('Internal error - incorrectly formatted modifyValue button ID');
    switch (request) {
        case 'cancel':
            // Close the interaction panel and replace the original clickable value.
            panel.replaceWith(varValueDisplay.call(vars[position], value));
            break;
        case 'remove':
            // Replace the value with null (missing value).
            vars[position].v.forEach(function(v, index, list) { if (v == value) list[index] = null } );
            vars[position].scanValues();
            displayVariables();  // refresh, which will zap our interaction panel
            break;
        case 'replace':
            // Query for a replacement value.
            panel.html('Replace ' + safe(value) + ' with number ' + '<input name="replaceWith">' +
                    '<button type="button" onclick="replaceValOK(' + position + ", '" + safe(value) + "'" + ', this)">OK</button>' +
                    '<button type="button" onclick="replaceValCancel(' + "'" + safe(panelId) + "','" + safe(value) + "'," + position + ')">cancel</button>');
            break;
    }
}

function replaceValOK(position, oldValue, button) {
    var newValue = $(button).siblings('input[name=replaceWith]').val();
    if (newValue == '' || isNaN(Number(newValue)))
        alert('Please enter a number.');
    else {
        newValue = Number(newValue);
        vars[position].v.forEach(function(v, index, list) { if (v == oldValue) list[index] = newValue } );
        vars[position].scanValues();
        displayVariables();
    }
}

function replaceValCancel(panelId, oldValue, position) {
    // Remove interaction panel and replace old clickable value.
    $('#' + panelId).replaceWith(varValueDisplay.call(vars[position], oldValue));

}

function numOrder(a, b) { return a - b; }  // for numeric sorting

function safe(s) {
    // Return an HTML-safe string with character entities.
    s = s.replace(/&/g, '&amp;');
    s = s.replace(/</g, '&lt;');
    s = s.replace(/>/g, '&gt;');
    s = s.replace(/"/g, '&quot;');
    return s;
}

function unSafe(s) {
    // Reverse HTML character entities.
    s = s.replace(/&amp;/g, '\\&');
    s = s.replace(/&lt;/g, '<');
    s = s.replace(/&gt;/g, '>');
    s = s.replace(/&quot;/g, '"');
    return s;
}

/*********
function mkScale(v) {
    // Make a vertical scaling function for this variable.
    // Create hybrid ranges for mixed text/numeric variables.
    // Assign text values positive or negative ordinals, with positives
    //   appearing above the numeric range and negatives appearing below.
    // [Later, allow ordinal or mixed domains to be sorted interactively.]
    switch (v.type) {
        case 'empty':
            v.scale = null;
            break;
        case 'SASID':
        case 'identifier':
        case 'category':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort()).split(0, h);
            break;
        case 'grade level':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort(numOrder)).split(0, h);
            break;
        case 'score':
            if (v.uniqCount == v.uniqNumCount)
                v.scale = pv.Scale.linear(v.min, v.max).range(0, h);
            else if (v.uniqTextCount == 0) {
                // Make a place on the scale for missing (null) values.
                var ls = pv.Scale.linear(v.min, v.max).range(0.1 * h, h);
                v.scale = function(val) {
                    if (val === null) return 0;
                    return ls(val);
                };
            }
            else {
                // Make places for text values and missing values.
                var u = pv.uniq(data, function(row) { return row[v.position] } );
                var tShare = (v.uniqText + v.hasEmpty) / v.uniqNumCount;  // text share of the axis
                var ts = pv.Scale.ordinal(u).split(0, tShare * h);
                var ls = pv.Scale.linear(v.min, v.max).range((1 - tShare) * h, h);
                v.scale = function(val) {
                    if (val === null)
                };
            }
            break;
        case 'MCAS scaled score':
            v.scale = pv.Scale.linear(200, 280).range(0, h);
            break;
    }
}

function mkVisibility(v) {
    v.visMin = null;
    v.visMax = null;
    v.visible = function(val) { 
        if (v.visMin === null)
            return true;
        var y = v.scale(val); 
        return y >= v.visMin && y <= v.visMax ;
    }
}
*********/
/*********
function showVar(checkbox) {
    // Called by a variable's 'show' checkbox when it changes.
    var nameExp = /column(\d+)Show/;  // parse the checkbox name for column position
    var match = nameExp.exec(checkbox.name);
    var column = Number(match[1]);  // column position
    if (checkbox.checked)
        displayCols.push(column);  // add to active display list
    else
        displayCols.splice(displayCols.indexOf(column), 1);  // remove from display list
    drawParallelCoords();  // redraw visualization
}

function rowVisible(row) {
    // Given a row from the data array, determine and return
    // whether it is 'visible' on all displayed variable axes.
    var dCol;  // index into displayCols
    var col;  // index into vars or row
    for (dCol = 0; dCol < displayCols.length; dCol++) {
        col = displayCols[dcol];
        if (!vars[col].visible(row[col]))
            return false;
    }
    return true;
}

var vis;
function drawParallelCoords() {
    // Draw our visualization.
    var x = pv.Scale.ordinal(displayCols).split(0, w);
    vis = new pv.Panel()
        .canvas('visDiv')
        .width(w)
        .height(h)
        .left(padding.left)
        .right(padding.right)
        .top(padding.top)
        .bottom(padding.bottom)
        .fillStyle('lightgray')
        ;
    vis.add(pv.Panel)  // display all occurrences (rows)
        .data(data)
        .add(pv.Line)
        .data(displayCols)
        .left(x)
        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
        .strokeStyle('gray')
        .add(pv.Dot)
        .fillStyle('aliceblue')
        .title( function(dCol, row) { return row[displayCols[dCol]] } )
        ;
//    var selected = vis.add(pv.Panel)  // highlight selected occurrences
//        .data(data)
//        .visible(rowVisible)
//        .add(pv.Line)
//        .data(displayCols)
//        .left(x)
//        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
//        .strokeStyle('blue')
//        .add(pv.Dot)
//        .fillStyle('lightblue')
//        .title( function(col, row) { return row[col] } )
//        ;
    vis.add(pv.Label)  // axis labels
        .data(displayCols)
        .bottom(-5)
        .textBaseline('top')
        .left(x)
        .textAlign('center')
        .font("12px sans-serif")
        .text( function(col) { return vars[col].label } )
        .fillStyle('lightgray')
//        .events('all')
//        .event('click', deselect)
        ;
    vis.add(pv.Panel)  // axis value labels for categories and grades
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['category','grade level'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.domain() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(label) { return label ? label : '(none)' } )
        ;
    vis.add(pv.Panel)  // axis value labels for scores
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['score','MCAS scaled score'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.ticks() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(val, col) { return vars[col].scale.tickFormat(val) } )
        ;
//    var selectZone = vis.add(pv.Panel)
//        .data(displayCols.map(function(col) { return {y:0, dy:h, col:col}; }))
//        .left(function(d) x(d.col) - 30)
//        .width(20)
//        .fillStyle("rgba(0,0,0,.001)")
//        .cursor("crosshair")
//        .event("mousedown", pv.Behavior.select())
//        .event("select", update)
//        .event("selectend", selectAll)
//        ;
    vis.render();

    // If our visualization div is hidden, bring it into view.
    if ($('#visDiv').css('display') == 'none')
        $('#visDiv').slideDown();
//    function update(d) {
//        vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
//        vars[d.col].visMax = Math.min(h, h - d.y);
//        vis.render();
//    }
//    function selectAll(d) {
//        console.log(d.dy);
//        if (d.dy != 0) {
//            vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
//            vars[d.col].visMax = Math.min(h, h - d.y);
//            vis.render();
//        } else {
//            deselect(d.col);
//        }
//    }
//    function deselect(col) {
//        vars[col].minVis = null;
//        vars[col].maxVis = null;
//        vis.render();
//    }
}
*********/

</script>
